/**
 * Copyright (c) 2009 Noel Ang
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */
package com.lunarshades.samsara.ui;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTable;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.WindowConstants;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.text.JTextComponent;

import com.lunarshades.samsara.Raider;
import com.lunarshades.samsara.Recorder;
import com.lunarshades.samsara.Samsara;
import com.lunarshades.samsara.ui.actions.DeselectAllAction;
import com.lunarshades.samsara.ui.actions.KarmaAwardAction;
import com.lunarshades.samsara.ui.actions.KarmaChangeAction;
import com.lunarshades.samsara.ui.actions.KarmaChangeEvent;
import com.lunarshades.samsara.ui.actions.KarmaChangeListener;
import com.lunarshades.samsara.ui.actions.KarmaContestAction;
import com.lunarshades.samsara.ui.actions.KarmaDeductAction;
import com.lunarshades.samsara.ui.actions.KarmaStatusEvent;
import com.lunarshades.samsara.ui.actions.LoadDataAction;
import com.lunarshades.samsara.ui.actions.RaiderAddNameAction;
import com.lunarshades.samsara.ui.actions.RosterAddAction;
import com.lunarshades.samsara.ui.actions.RosterChangeEvent;
import com.lunarshades.samsara.ui.actions.RosterChangeListener;
import com.lunarshades.samsara.ui.actions.RosterExportAction;
import com.lunarshades.samsara.ui.actions.RosterFilterAction;
import com.lunarshades.samsara.ui.actions.RosterImportAction;
import com.lunarshades.samsara.ui.actions.RosterRemoveAction;
import com.lunarshades.samsara.ui.actions.SaveAsDataAction;
import com.lunarshades.samsara.ui.actions.SaveDataAction;
import com.lunarshades.samsara.ui.actions.SelectAllAction;
import com.lunarshades.samsara.ui.actions.SelectInvertAction;
import org.ahmadsoft.ropes.Rope;

/**
 * Main container for Samasara GUI.
 *
 * @author Noel Ang sparta.variable@gmail.com
 */
public class SamsaraFrame extends JFrame
        implements TableModelListener, PropertyChangeListener,
                   KarmaChangeListener, RosterChangeListener,
                   ListSelectionListener {

public SamsaraFrame()
        throws UnsupportedLookAndFeelException, IllegalAccessException {
    initComponent();
    loadPreferences();
}

private void loadPreferences() {
    Preferences prefs = Preferences.userNodeForPackage(this.getClass());

    try {
        prefs.sync();
    } catch (BackingStoreException e) {
        // Ignore sync failures
    }

    int width = prefs.getInt("width", getPreferredSize().width);
    int height = prefs.getInt("height", getPreferredSize().height);
    int positionX = prefs.getInt("x", getLocation().x);
    int positionY = prefs.getInt("y", getLocation().y);

    Point location = calcLocation(width, height, positionX, positionY);
    Dimension dim = calcDimension(width, height, location.x, location.y);
    setSize(dim);
    setPreferredSize(dim);
    setLocation(location);

    mActionSaveData.loadPreferences(prefs);
    mActionLoadData.loadPreferences(prefs);
}

private void savePreferences() {
    Preferences prefs = Preferences.userNodeForPackage(this.getClass());

    Dimension size = getSize();
    prefs.putInt("width", size.width);
    prefs.putInt("height", size.height);

    Point location = getLocation();
    prefs.putInt("x", location.x);
    prefs.putInt("y", location.y);

    mActionSaveData.savePreferences(prefs);
    mActionLoadData.savePreferences(prefs);

    try {
        prefs.flush();
    } catch (BackingStoreException e) {
        // Ignore flush failures
    }
}

private Point calcLocation(int width, int height, int x, int y) {
    Toolkit tk = Toolkit.getDefaultToolkit();
    Dimension screenSize = tk.getScreenSize();
    if (x + width >= screenSize.width) {
        x = (width > screenSize.width ? width : screenSize.width) - (width
                < screenSize.width ? width : screenSize.width);
    }
    if (y + height >= screenSize.height) {
        y =
                (height > screenSize.height ? height : screenSize.height) - (
                        height < screenSize.height
                        ? height
                        : screenSize.height);
    }
    return new Point(x, y);
}

private Dimension calcDimension(int width, int height, int x, int y) {
    Toolkit tk = Toolkit.getDefaultToolkit();
    Dimension screenSize = tk.getScreenSize();
    if (x + width >= screenSize.width) {
        width = (x > screenSize.width ? x : screenSize.width) - (x
                < screenSize.width ? x : screenSize.width);
    }
    if (y + height >= screenSize.height) {
        height = (y > screenSize.height ? y : screenSize.height) - (y
                < screenSize.height ? y : screenSize.height);
    }
    return new Dimension(width, height);
}

private void initComponent()
        throws UnsupportedLookAndFeelException, IllegalAccessException {
    setTitle("Samsara");
    setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
    addWindowListener(new WindowAdapter() {
        @Override
        public void windowClosing(WindowEvent e) {
            mActionQuitApp.actionPerformed(null);
        }
    }
    );

    // Membership roster/table
    mRosterTable = new JTable();
    RosterRowSelectionModel model = new RosterRowSelectionModel();
    mRosterTable.setSelectionModel(model);
    mRosterTableModel = RosterTableModel.createModel(
            mRosterTable,
            Raider.class
    );
    mRosterTableModel.addTableModelListener(this);

    initActions();
    initMenu();
    initLayout();
}

private void initActions() {
    mActionLogging = new StartLoggingAction();
    mActionSaveData = new SaveDataAction<Raider>(mRosterTableModel, this);
    mActionSaveAsData = new SaveAsDataAction(mActionSaveData);
    mActionDataExport = new RosterExportAction(mRosterTableModel, this);
    mActionShowLoggingConsole =
            new LoggingConsoleAction(mRecorder.getLoggingConsoleView());

    mActionLoadData = new LoadDataAction<Raider>(mRosterTableModel,
            Raider.class,
            mActionSaveData,
            this
    );
    mActionDataImport = new RosterImportAction<Raider>(mRosterTableModel,
            Raider.class,
            mActionSaveData,
            this
    );

    mActionAddMember =
            new RosterAddAction<Raider>(mRosterTableModel, Raider.class, this);
    mActionRemoveMember = new RosterRemoveAction<Raider>(mRosterTableModel,
            mRosterTable,
            this
    );
    mActionAddName =
            new RaiderAddNameAction<Raider>(mRosterTableModel, mRosterTable);
    mActionContest =
            new KarmaContestAction(mRosterTableModel, mRosterTable, this);
    mActionKarmaAward = new KarmaAwardAction(mRosterTableModel, mRosterTable);
    mActionKarmaDeduct = new KarmaDeductAction(mRosterTableModel, mRosterTable);

    mActionSelectAll = new SelectAllAction(mRosterTable);
    mActionDeselectAll = new DeselectAllAction(mRosterTable);
    mActionSelectInvert = new SelectInvertAction(mRosterTable);
    mActionFilterMembers = new RosterFilterAction(mRosterTable);
    mActionDefilterMembers = mActionFilterMembers.getDefilterAction();

    // ======================
    // Accelerators
    // ======================

    mActionFilterMembers.putValue(Action.ACCELERATOR_KEY,
            KeyStroke.getKeyStroke(KeyEvent.VK_F,
                    java.awt.event.InputEvent.CTRL_DOWN_MASK
            )
    );
    mActionDefilterMembers.putValue(Action.ACCELERATOR_KEY,
            KeyStroke.getKeyStroke(KeyEvent.VK_G,
                    java.awt.event.InputEvent.CTRL_DOWN_MASK
            )
    );
    mActionContest.putValue(Action.ACCELERATOR_KEY,
            KeyStroke.getKeyStroke(KeyEvent.VK_K,
                    java.awt.event.InputEvent.CTRL_DOWN_MASK
            )
    );
    mActionSelectAll.putValue(Action.ACCELERATOR_KEY,
            KeyStroke.getKeyStroke(KeyEvent.VK_A,
                    java.awt.event.InputEvent.CTRL_DOWN_MASK
            )
    );
    mActionDeselectAll.putValue(Action.ACCELERATOR_KEY,
            KeyStroke.getKeyStroke(KeyEvent.VK_D,
                    java.awt.event.InputEvent.CTRL_DOWN_MASK
            )
    );
    mActionSelectInvert.putValue(Action.ACCELERATOR_KEY,
            KeyStroke.getKeyStroke(KeyEvent.VK_V,
                    java.awt.event.InputEvent.CTRL_DOWN_MASK
            )
    );
    mActionShowLoggingConsole.putValue(Action.ACCELERATOR_KEY,
            KeyStroke.getKeyStroke(KeyEvent.VK_C,
                    java.awt.event.InputEvent.CTRL_DOWN_MASK
            )
    );
    mActionLoadData.putValue(Action.ACCELERATOR_KEY,
            KeyStroke.getKeyStroke(KeyEvent.VK_L,
                    java.awt.event.InputEvent.CTRL_DOWN_MASK
            )
    );
    mActionSaveData.putValue(Action.ACCELERATOR_KEY,
            KeyStroke.getKeyStroke(KeyEvent.VK_S,
                    java.awt.event.InputEvent.CTRL_DOWN_MASK
            )
    );

    // ======================
    // Listener relationships
    // ======================

    // I forego table model listening for property listening for the following
    // needs because the granularity is too fine with TableModelListener.

    // Frame interested in:
    // - Save action to clear dirty flag
    // - Load action to clear dirty flag and logging
    // - Import action to set dirty flag
    // - Add, remove, contest karma for logging
    // - Add, remove roster entries for logging
    // - Filter/defilter to update status bar
    // - Table selection model changes to update status bar
    mActionSaveData.addPropertyChangeListener(this);
    mActionLoadData.addPropertyChangeListener(this);
    mActionDataImport.addPropertyChangeListener(this);
    mActionKarmaAward.addKarmaChangeListener(this);
    mActionKarmaDeduct.addKarmaChangeListener(this);
    mActionContest.addKarmaChangeListener(this);
    mActionAddMember.addRosterChangeListener(this);
    mActionRemoveMember.addRosterChangeListener(this);
    mActionFilterMembers.addPropertyChangeListener(this);
    mActionDefilterMembers.addPropertyChangeListener(this);
    mRosterTable.getSelectionModel().addListSelectionListener(this);

    // Save action interested in:
    // - Table data updates to enable/disable itself
    // - Load action to enable/disable itself
    // - Import action to reset save (file) target, if any, and enable/disable
    addPropertyChangeListener(mActionSaveData);
    mActionLoadData.addPropertyChangeListener(mActionSaveData);
    mActionDataImport.addPropertyChangeListener(mActionSaveData);

    // SaveAs action interested in:
    // - Load action to enable/disable itself
    // - Table data updates to enable/disable itself
    // - Import action to enable/disable itself
    addPropertyChangeListener(mActionSaveAsData);
    mActionLoadData.addPropertyChangeListener(mActionSaveAsData);
    mActionDataImport.addPropertyChangeListener(mActionSaveAsData);

    // Filter action interested in:
    // - Load action to re-apply filtering
    mActionLoadData.addPropertyChangeListener(mActionFilterMembers);

    // =================================
    // Roster Table interface extensions
    // =================================
    
    // Key mapping:
    // ENTER -> select
    // SPACE -> select
    RosterRowSelectionModel selModel =
            (RosterRowSelectionModel) mRosterTable.getSelectionModel();
    final Action selectAction = new RosterSelectToggle(selModel);
    ActionMap actionMap = mRosterTable.getActionMap();
    actionMap.put(RosterSelectToggle.ACTION_COMMAND, selectAction);
    InputMap inputMap =
            mRosterTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
    inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0),
            RosterSelectToggle.ACTION_COMMAND
    );
    inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
            RosterSelectToggle.ACTION_COMMAND
    );
    // Replace Control+C key mapping to avoid collision with Console Log
    // accelerator.
    inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C,
            java.awt.event.InputEvent.CTRL_DOWN_MASK
    ), LoggingConsoleAction.ACTION_COMMAND
    );
    actionMap.put(LoggingConsoleAction.ACTION_COMMAND,
            mActionShowLoggingConsole
    );

    // Select action via double-click
    mRosterTable.addMouseListener(new MouseAdapter() {
        @Override
        public void mouseClicked(MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() > 1) {
                ActionEvent EVENT = new ActionEvent(mRosterTable, 0, "");
                selectAction.actionPerformed(EVENT);
            }
        }
    });

    // Incremental search-navigation via keys typed
    mRosterTable.addKeyListener(new TableSearchNavigator(0,
            1250L,
            new TableSearchNavigator.Comparator() {
                public boolean match(Object key, Object value) {
                    boolean matches = false;
                    if (key instanceof Rope) {
                        CharSequence chars = value.toString();
                        Rope keyRope = (Rope) key;
                        keyRope.trim();
                        matches = true;
                        for (int i = 0, end =
                                Math.min(keyRope.length(), chars.length());
                             matches && i < end;
                             ++i) {
                            char kc = Character.toUpperCase(keyRope.charAt(i));
                            char vc = Character.toUpperCase(chars.charAt(i));
                            matches = (kc == vc);
                        }
                    }
                    return matches;
                }
            }
    )
    );
    
    // Context menu via:
    // 1. Mouse right-click
    // 2. Special "menu"/"application" key (Windows-oriented keyboards)
    // 3. '?' key
    mRosterTable.addMouseListener(new MouseAdapter() {
        @Override
        public void mouseClicked(MouseEvent e) {
            if (SwingUtilities.isRightMouseButton(e)
                    && e.getClickCount() == 1) {
                JPopupMenu menu = mRosterTable.getComponentPopupMenu();
                if (menu != null) {
                    Point location = mRosterTable.getPopupLocation(e);
                    if (location != null) {
                        menu.show(mRosterTable, location.x, location.y);
                    } else {
                        menu.setInvoker(mRosterTable);
                        menu.setVisible(true);
                    }
                }
            }
        }
    });
    mRosterTable.addKeyListener(new KeyAdapter() {
        private void showMenu() {
            int lead = mRosterTable.getSelectionModel().getLeadSelectionIndex();
            JPopupMenu menu = mRosterTable.getComponentPopupMenu();
            if (menu != null) {
                Rectangle rect = mRosterTable.getCellRect(lead, 0, true);
                menu.show(mRosterTable, rect.x, rect.y);
            }
        }
        @Override
        public void keyTyped(KeyEvent e) {
            if (e.getKeyChar() == '?') {
                showMenu();
            }
        }

        @Override
        public void keyPressed(KeyEvent e) {
            int code = e.getKeyCode();
            switch (code) {
            case KeyEvent.VK_CONTEXT_MENU:
                showMenu();
                break;
            case KeyEvent.VK_F10:
                if (e.isShiftDown()) {
                    showMenu();
                    e.consume();
                }
                break;
            }
        }
    });
}

private void initMenu() {
    final JMenuBar menubar = new JMenuBar();
    setJMenuBar(menubar);

    JMenu menu;
    JMenu innerMenu;

    // File menu
    menu = new JMenu("File");
    menu.setMnemonic('f');
    menu.add(new JMenuItem(mActionLoadData));
    menu.add(new JMenuItem(mActionSaveData));
    menu.add(new JMenuItem(mActionSaveAsData));
    menu.add(new JSeparator());
    menu.add(new JMenuItem(mActionQuitApp));
    menubar.add(menu);

    // Edit menu
    menu = new JMenu("Edit");
    menu.setMnemonic('e');
    menu.add(new JMenuItem(mActionAddMember));
    menu.add(new JMenuItem(mActionRemoveMember));
    menu.add(new JMenuItem(mActionAddName));
    menu.add(new JSeparator());
    menu.add(new JMenuItem(mActionDefilterMembers));
    menu.add(new JMenuItem(mActionFilterMembers));
    menu.add(new JSeparator());
    menu.add(new JMenuItem(mActionDeselectAll));
    menu.add(new JMenuItem(mActionSelectAll));
    menu.add(new JMenuItem(mActionSelectInvert));
    menubar.add(menu);

    // Action menu
    menu = new JMenu("Actions");
    menu.setMnemonic('c');
    menu.add(new JMenuItem(mActionKarmaAward));
    menu.add(new JMenuItem(mActionKarmaDeduct));
    menu.add(new JSeparator());
    menu.add(new JMenuItem(mActionContest));
    menubar.add(menu);

    // Tools menu
    menu = new JMenu("Tools");
    menu.setMnemonic(KeyEvent.VK_T);
    innerMenu = new JMenu("Logging");
    innerMenu.setMnemonic(KeyEvent.VK_L);
    innerMenu.add(new JMenuItem(mActionLogging));
    innerMenu.add(new JSeparator());
    innerMenu.add(new JMenuItem(mActionShowLoggingConsole));
    menu.add(innerMenu);
    innerMenu = new JMenu("Import from");
    innerMenu.setMnemonic(KeyEvent.VK_I);
    innerMenu.add(new JMenuItem(mActionDataImport));
    menu.add(innerMenu);
    innerMenu = new JMenu("Export to");
    innerMenu.setMnemonic(KeyEvent.VK_E);
    innerMenu.add(new JMenuItem(mActionDataExport));
    menu.add(innerMenu);
    menubar.add(menu);

    // Help menu
    menu = new JMenu("Help");
    menu.setMnemonic(KeyEvent.VK_H);
    menu.add(new JMenuItem(mActionAppInfo));
    menubar.add(menu);

    // ==============================
    // Context menu for table entries
    // ==============================
    final JPopupMenu contextMenu = new JPopupMenu();
    contextMenu.add(mActionAddName);
    contextMenu.add(mActionKarmaAward);
    contextMenu.add(mActionKarmaDeduct);
    contextMenu.addSeparator();
    contextMenu.add(mActionFilterMembers);
    contextMenu.add(mActionDefilterMembers);
    contextMenu.add(mActionSelectAll);
    contextMenu.add(mActionDeselectAll);
    contextMenu.addSeparator();
    contextMenu.add(mActionContest);
    mRosterTable.setComponentPopupMenu(contextMenu);
}

private void initLayout() {
    setLayout(new GridBagLayout());

    GridBagConstraints constraints;

    // Toolbar
    constraints = new GridBagConstraints();
    constraints.fill = GridBagConstraints.BOTH;
    constraints.gridx = 0;
    constraints.weightx = 1.0;
    JToolBar toolbar = new JToolBar(JToolBar.HORIZONTAL);
    toolbar.setFloatable(false);
    toolbar.add(mActionLoadData);
    toolbar.add(mActionSaveData);
    toolbar.addSeparator();
    toolbar.add(mActionFilterMembers);
    toolbar.add(mActionDefilterMembers);
    toolbar.add(mActionDeselectAll);
    toolbar.addSeparator();
    toolbar.add(mActionAddMember);
    toolbar.add(mActionKarmaAward);
    toolbar.add(mActionContest);
    add(toolbar, constraints);

    // Membership roster
    constraints = new GridBagConstraints();
    constraints.fill = GridBagConstraints.BOTH;
    constraints.gridx = 0;
    constraints.weightx = 1.0;
    constraints.weighty = 1.0;
    JScrollPane scrollPane = new JScrollPane(mRosterTable,
            JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
            JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED
    );
    add(scrollPane, constraints);

    // Status bar
    constraints = new GridBagConstraints();
    constraints.fill = GridBagConstraints.HORIZONTAL;
    constraints.gridx = 0;
    constraints.weightx = 1.0;
    add(mStatusPanel.getComponent(), constraints);
}

private synchronized void setIsDataSaved(boolean isSaved) {
    mIsDataSaved = isSaved;
    if (isSaved) {
        firePropertyChange(DATA_CLEAN, null, null);
    } else {
        firePropertyChange(DATA_MODIFIED, null, null);
    }
}

private synchronized void logStatus() {
    for (Raider r : mRosterTableModel.sortedSet()) {
        KarmaStatusEvent<Raider> e = new KarmaStatusEvent<Raider>(r);
        mRecorder.log(e);
    }
}

private File chooseFile() {
    File targetFile = null;
    int decision = mFileChooser.showSaveDialog(this);
    if (decision == JFileChooser.APPROVE_OPTION) {
        targetFile = mFileChooser.getSelectedFile();
    }
    return targetFile;
}

private synchronized boolean startLogging() {
    File logFile = chooseFile();
    if (logFile != null) {
        try {
            mRecorder.start(logFile);
            logStatus();
            mStatusPanel.report(StatusPanel.StatusType.LOG, Boolean.TRUE);
            firePropertyChange(LOGGING_STARTED, null, null);
        } catch (Exception e) {
            StringBuffer buffer = new StringBuffer();
            buffer.append("Unable to open log file.\n");
            buffer.append(e.getLocalizedMessage());
            JOptionPane.showMessageDialog(this,
                    buffer.toString(),
                    "Logging",
                    JOptionPane.ERROR_MESSAGE
            );
        }
    }
    return mRecorder.isStarted();
}

private synchronized boolean stopLogging() {
    if (mRecorder.isStarted()) {
        mRecorder.stop();
        JOptionPane.showMessageDialog(this,
                "Logging ended.",
                "Logging",
                JOptionPane.INFORMATION_MESSAGE
        );
        mStatusPanel.report(StatusPanel.StatusType.LOG, Boolean.FALSE);
        firePropertyChange(LOGGING_STOPPED, null, null);
    }
    return !mRecorder.isStarted();
}

private final StringBuffer mPostBuffer = new StringBuffer(SELECTED_ROWS_TEXT);
private volatile int mLastSelectedRowsCount = 0;
private volatile String mSelectedRowsMessage;
private void postRowsSelected(int i) {
    synchronized (mPostBuffer) {
        if (i != mLastSelectedRowsCount && i >= 0) {
            mLastSelectedRowsCount = i;
            mPostBuffer.delete(SELECTED_ROWS_TEXT.length(),
                    mPostBuffer.length()
            );
            mPostBuffer.append(i);
            mSelectedRowsMessage = mPostBuffer.toString();
        }
        postStatusMessage(mSelectedRowsMessage);
    }
}

private void postRecordCount() {
    int visibleRows = mRosterTableModel.getVisibleRowCount();
    mStatusPanel.report(StatusPanel.StatusType.DATA_SET_SIZE, visibleRows);
}

public void postStatusMessage(String message) {
    mStatusPanel.report(StatusPanel.StatusType.GENERAL, message);
}

public int promptToSaveData() {
    return JOptionPane.showConfirmDialog(this,
            "You have unsaved data. Would you like to save it before continuing?",
            "Saving",
            JOptionPane.YES_NO_CANCEL_OPTION,
            JOptionPane.WARNING_MESSAGE
    );
}

public synchronized boolean isDataSaved() {
    return mIsDataSaved;
}

/**
 * This method gets called when a bound property is changed.
 *
 * @param evt A PropertyChangeEvent object describing the event source and the
 *            property that has changed.
 */
public void propertyChange(PropertyChangeEvent evt) {
    String property = evt.getPropertyName();

    if (LoadDataAction.FILE_LOADED.equals(property)) {
        mRosterTableModel.fireTableDataChanged();
        setIsDataSaved(true);
        // If logging was active, deactivate it.
        if (mRecorder.isStarted()) {
            mActionLogging.actionPerformed(null);
        }
        mRosterTable.requestFocusInWindow();
    }
    if (SaveDataAction.FILE_SAVED.equals(property)) {
        setIsDataSaved(true);
        mRosterTable.requestFocusInWindow();
    }
    if (RosterImportAction.DATA_IMPORTED.equals(property)) {
        mRosterTableModel.fireTableDataChanged();
        mRosterTable.requestFocusInWindow();
    }
    if (RosterFilterAction.FILTER_PROPERTY.equals(property)
            || RosterFilterAction.DEFILTER_PROPERTY.equals(property)) {
        boolean wasSaved = isDataSaved();
        mRosterTableModel.fireTableDataChanged();
        setIsDataSaved(wasSaved);
        mRosterTable.requestFocusInWindow();
    }
    postRecordCount();
}

public void tableChanged(TableModelEvent e) {
    setIsDataSaved(mRosterTable.getModel().getRowCount() == 0);
    postRecordCount();
}

public void karmaChanged(KarmaChangeEvent<?> e) {
    mRecorder.log(e);
}

public void rosterChanged(RosterChangeEvent<?> e) {
    mRecorder.log(e);
}

public void valueChanged(ListSelectionEvent e) {
    if (!e.getValueIsAdjusting()) {
        int selectCount = mRosterTable.getSelectedRowCount();
        postRowsSelected(selectCount);
    }
}

private class AboutAppAction extends AbstractAction {
    private AboutAppAction() {
        super(NAME);
        putValue(Action.SHORT_DESCRIPTION, SHORT_TEXT);
        putValue(Action.MNEMONIC_KEY, MNEMONIC);
    }

    private JComponent message() {
        final String RES = "/"
                + SamsaraFrame.this.getClass().getName().replace('.', '/')
                + ".class";

        URL url = getClass().getResource(RES);
        String urlStr = url.toExternalForm();
        urlStr = urlStr.substring(0, urlStr.indexOf(RES)) + "/META-INF/MANIFEST.MF";
        InputStream is = null;
        try {
            url = new URL(urlStr);
            is = (InputStream) url.getContent(new Class[] { InputStream.class });
        } catch (IOException e) {
            throw new RuntimeException("Application manifest missing");
        }
        if (is == null) {
            throw new RuntimeException("Application manifest missing");
        }

        Manifest man;
        Attributes attr;
        String name;
        String version;
        try {
            man = new Manifest(is);
            attr = man.getAttributes("com/lunarshades/samsara");
            if ((name = attr.getValue("Implementation-Title")) == null) {
                name = Samsara.class.getSimpleName();
            }
            if ((version = attr.getValue("Implementation-Version")) == null) {
                version = "?";
            }
        } catch (Exception e) {
            throw new RuntimeException("Manifest access error", e);
        } finally {
            try {
                is.close();
            } catch (IOException ignored) {
            }
        }

        JPanel panel = new JPanel(new GridBagLayout());
        GridBagConstraints cons = new GridBagConstraints();
        Font boldFont = panel.getFont().deriveFont(Font.BOLD);
        cons.gridx = 0;
        cons.weightx = 1;
        cons.insets = new Insets(4, 0, 0, 0);

        JLabel label;

        label = new JLabel(name);
        label.setFont(boldFont);
        cons.weighty = 0.5;
        cons.gridheight = 2;
        cons.anchor = GridBagConstraints.SOUTH;
        panel.add(label, cons);

        cons.weighty = 0;
        cons.gridheight = 1;
        cons.anchor = GridBagConstraints.CENTER;
        panel.add(new JLabel(VERSION_TEXT.concat(" ").concat(version)), cons);

        cons.weighty = 0.5;
        cons.gridheight = 2;
        cons.anchor = GridBagConstraints.NORTH;
        panel.add(new JLabel(AUTHOR_TEXT), cons);

        return panel;
    }

    private JDialog getDialog() {
        if (mDialog == null) {
            JOptionPane pane = new JOptionPane();
            pane.setMessage(message());
            pane.setMessageType(JOptionPane.PLAIN_MESSAGE);
            mDialog = pane.createDialog(SamsaraFrame.this, NAME);
        }
        return mDialog;
    }

    public void actionPerformed(ActionEvent e) {
        getDialog().setVisible(true);
    }

    private static final String NAME = "About";
    private static final String SHORT_TEXT = "Program information";
    private static final String AUTHOR_TEXT = "Noel Ang (sparta.variable@gmail.com)";
    private static final String VERSION_TEXT = "Version";
    private static final int MNEMONIC = KeyEvent.VK_A;
    private volatile JDialog mDialog;
}

private class QuitAppAction extends AbstractAction {

    QuitAppAction() {
        super(NAME);
        URL smallIconUrl = getClass().getResource("icons/stop16.gif");
        putValue(Action.SMALL_ICON, new ImageIcon(smallIconUrl));
        putValue(Action.SHORT_DESCRIPTION, SHORT_TEXT);
        putValue(Action.MNEMONIC_KEY, MNEMONIC);
    }

    public void actionPerformed(ActionEvent e) {
        if (!isDataSaved()) {
            int decision = promptToSaveData();
            switch (decision) {
            case JOptionPane.NO_OPTION:
                // Don't save
                break;
            case JOptionPane.YES_OPTION:
                // Save
                if (mActionSaveData.execute()) {
                    break;
                }
                // fall-through
            default:
                // Cancel quit.
                return;
            }
        }
        savePreferences();
        stopLogging();
        dispose();
    }

    private static final String NAME = "Quit";
    private static final String SHORT_TEXT = "Exit the application";
    private static final int MNEMONIC = KeyEvent.VK_Q;
}

private class StartLoggingAction extends AbstractAction {

    private StartLoggingAction() {
        super(START_LABEL);
        putValue(Action.MNEMONIC_KEY, START_KEY);
    }

    private void becomeStarter() {
        putValue(Action.NAME, START_LABEL);
        putValue(Action.MNEMONIC_KEY, START_KEY);
    }

    private void becomeStopper() {
        putValue(Action.NAME, STOP_LABEL);
        putValue(Action.MNEMONIC_KEY, STOP_KEY);
    }

    public void actionPerformed(ActionEvent e) {
        if (mRecorder.isStarted()) {
            if (stopLogging()) {
                postStatusMessage(STOP_STATUS);
                becomeStarter();
            }
        } else {
            if (startLogging()) {
                postStatusMessage(START_STATUS);
                becomeStopper();
            }
        }
    }

    private static final String STOP_STATUS = "Logging stopped.";
    private static final String START_STATUS = "Logging started.";
    private static final String START_LABEL = "Start Log";
    private static final String STOP_LABEL = "End Log";
    private static final int START_KEY = KeyEvent.VK_S;
    private static final int STOP_KEY = KeyEvent.VK_E;
}

private class LoggingConsoleAction extends AbstractAction {
    public LoggingConsoleAction(JTextComponent view) {
        super(CONSOLE_LABEL);
        putValue(Action.MNEMONIC_KEY, CONSOLE_KEY);
        mView = view;
    }

    private void showConsole() {
        if (mDialog == null) {
            JScrollPane scrollPane = new JScrollPane(mView,
                    JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
                    JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED
            );
            scrollPane.setPreferredSize(new Dimension(600, 200));

            JPanel panel = new JPanel(new GridBagLayout());
            GridBagConstraints c = new GridBagConstraints();
            c.fill = GridBagConstraints.BOTH;
            c.weightx = c.weighty = 1;
            panel.add(scrollPane, c);

            JOptionPane pane = new JOptionPane();
            pane.setMessageType(JOptionPane.PLAIN_MESSAGE);
            pane.setMessage(panel);
            mDialog = pane.createDialog(SamsaraFrame.this, CONSOLE_TITLE);
            mDialog.setModal(false);
            mDialog.pack();
        }
        mDialog.setVisible(true);
    }

    public void actionPerformed(ActionEvent e) {
        showConsole();
    }

    private static final String CONSOLE_LABEL = "Console";
    private static final String CONSOLE_TITLE = "Log Console";
    private static final int CONSOLE_KEY = KeyEvent.VK_C;
    private final JTextComponent mView;
    private volatile JDialog mDialog;

    public static final String ACTION_COMMAND = "showLogConsole";
}

private static final String SELECTED_ROWS_TEXT = "Selected rows: "; 
private RosterAddAction mActionAddMember;
private RosterRemoveAction mActionRemoveMember;
private RaiderAddNameAction mActionAddName;
private Action mActionDefilterMembers;
private RosterFilterAction mActionFilterMembers;
private LoadDataAction mActionLoadData;
private SaveDataAction mActionSaveData;
private SaveAsDataAction mActionSaveAsData;
private Action mActionLogging;
private final Action mActionQuitApp = new QuitAppAction();
private final Action mActionAppInfo = new AboutAppAction();
private Action mActionSelectAll;
private Action mActionDeselectAll;
private Action mActionSelectInvert;
private Action mActionDataExport;
private Action mActionDataImport;
private Action mActionShowLoggingConsole;
private KarmaChangeAction mActionContest;
private KarmaChangeAction mActionKarmaAward;
private KarmaChangeAction mActionKarmaDeduct;
private JTable mRosterTable;
private final StatusPanel mStatusPanel = new StatusPanel(true);
private final JFileChooser mFileChooser = new JFileChooser();
{
    mFileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
    mFileChooser.setMultiSelectionEnabled(false);
}
private final Recorder mRecorder = new Recorder();
private volatile RosterTableModel<Raider> mRosterTableModel;
private volatile boolean mIsDataSaved = true;

public static final String DATA_MODIFIED = "dataModified";
public static final String DATA_CLEAN = "dataClean";
public static final String LOGGING_STARTED = "LoggingStarted";
public static final String LOGGING_STOPPED = "LoggingStopped";
}
